﻿using System.Collections.Immutable;
using System.Security.Claims;
using AutoMapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using WT.OpenIddict.Application.Contracts.Authorization;
using WT.OpenIddict.Application.Contracts.Authorization.Dto;
using WT.OpenIddict.Application.Manager;
using WT.OpenIddict.Domain.User;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace WT.OpenIddict.Application.Authorization;

public class AuthorizationAppService : OpenIddictAppService, IAuthorizationAppService
{
    private readonly IOpenIddictApplicationManager _applicationManager;

    private readonly IOpenIddictScopeManager _scopeManager;

    private readonly SignInManager<UserDetails> _signInManager;

    private readonly UserManager<UserDetails> _userManager;


    public AuthorizationAppService(IOpenIddictApplicationManager applicationManager,
        UserManager<UserDetails> userManager, SignInManager<UserDetails> signInManager,
        IOpenIddictScopeManager scopeManager, IMapper objectMapper)
    {
        _applicationManager = applicationManager;
        _userManager = userManager;
        _signInManager = signInManager;
        _scopeManager = scopeManager;
        ObjectMapper = objectMapper;
    }

    public IMapper ObjectMapper { get; set; }

    public async Task<ClaimsPrincipal> GetClientCredentialsIdentityAsync(string clientId)
    {
        // 注意：客户端凭证会自动由OpenIddict进行验证：
        // 如果client_id或client_secret无效，此操作将不会被调用

        var application = await _applicationManager.FindByClientIdAsync(clientId) ??
                          throw new InvalidOperationException("The application cannot be found.");

        // 创建一个新的ClaimsIdentity，其中包含用于生
        // 成 id_token、token 或 code的声明.
        var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name,
            Claims.Role);

        // 使用 client_id 作为主题标识符
        identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
        identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));

        identity.SetDestinations(static claim => claim.Type switch
        {
            // 当授予了 “profile” 范围时，允许 “name” 声明被存储在
            // 访问令牌和身份令牌中 （通过调用 principal.SetScopes(...)）
            Claims.Name when claim.Subject.HasScope(Scopes.Profile)
                => [Destinations.AccessToken, Destinations.IdentityToken],

            // 否则，仅将该声明存储在访问令牌中
            _ => [Destinations.AccessToken]
        });
        return new ClaimsPrincipal(identity);
    }

    public async Task<PasswordIdentityResultDto> GetPasswordIdentityAsync(ImmutableArray<string> scopes,
        string? username, string? password)
    {
        var passwordIdentityResultDto = new PasswordIdentityResultDto();
        var user = await _userManager.FindByNameAsync(username);
        if (user == null) user = await _userManager.FindByEmailAsync(username);

        if (user == null)
        {
            passwordIdentityResultDto.CanSignIn = false;
            passwordIdentityResultDto.Message = "用户不存在";
            return passwordIdentityResultDto;
        }

        if (!await _userManager.CheckPasswordAsync(user, password))
        {
            passwordIdentityResultDto.CanSignIn = false;
            passwordIdentityResultDto.Message = "用户名或密码错误";
            return passwordIdentityResultDto;
        }

        //判断是否封禁
        if (user.Banned)
        {
            passwordIdentityResultDto.CanSignIn = false;
            passwordIdentityResultDto.Message = "账号被封禁，请联系管理员";
            return passwordIdentityResultDto;
        }

        passwordIdentityResultDto.Principal =
            await CreatePrincipalAsync(user, GrantTypes.Password, scopes);
        passwordIdentityResultDto.CanSignIn = true;
        return passwordIdentityResultDto;
    }

    private async Task<ClaimsPrincipal> CreatePrincipalAsync(UserDetails user, string grantType,
        ImmutableArray<string> scopes)
    {
        var principal = await _signInManager.CreateUserPrincipalAsync(user);

        // Note: in this sample, the granted scopes match the requested scope
        // but you may want to allow the user to uncheck specific scopes.
        // For that, simply restrict the list of scopes before calling SetScopes.
        principal.SetScopes(scopes);
        principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());

        foreach (var claim in principal.Claims) claim.SetDestinations(GetDestinations(grantType, claim));

        return principal;
    }

    private static string[] GetDestinations(string grantType, Claim claim)
    {
        string[] destinations = [];
        switch (grantType)
        {
            case GrantTypes.ClientCredentials:
                destinations = DestinationsUtils.GetClientCredentialsDestinations(claim);
                break;
            case GrantTypes.Password:
                destinations = DestinationsUtils.GetPasswordDestinations(claim);
                break;
        }

        return destinations;
    }
}